# -*- coding: binary -*-
require 'rex/proto/smb'
require 'rex/proto/ntlm'
require 'rex/proto/dcerpc'
require 'rex/encoder/ndr'

module Msf

require 'msf/core/exploit/smb_server'

###
#
# This mixin provides utility methods for interacting with a SMB/CIFS service on
# a remote machine.  These methods may generally be useful in the context of
# exploitation.  This mixin extends the Tcp exploit mixin. Only one SMB
# service can be accessed at a time using this class.
#
###

module Exploit::Remote::SMB

  include Exploit::Remote::Tcp
  include Exploit::Remote::NTLM::Client

  SIMPLE = Rex::Proto::SMB::SimpleClient
  XCEPT  = Rex::Proto::SMB::Exceptions
  CONST  = Rex::Proto::SMB::Constants


  # Alias over the Rex DCERPC protocol modules
  DCERPCPacket   = Rex::Proto::DCERPC::Packet
  DCERPCClient   = Rex::Proto::DCERPC::Client
  DCERPCResponse = Rex::Proto::DCERPC::Response
  DCERPCUUID     = Rex::Proto::DCERPC::UUID
  NDR            = Rex::Encoder::NDR

  def initialize(info = {})
    super

    register_evasion_options(
    [
      OptBool.new('SMB::pipe_evasion',     [ true, 'Enable segmented read/writes for SMB Pipes', false]),
      OptInt.new('SMB::pipe_write_min_size', [ true, 'Minimum buffer size for pipe writes',  1]),
      OptInt.new('SMB::pipe_write_max_size', [ true, 'Maximum buffer size for pipe writes', 1024]),
      OptInt.new('SMB::pipe_read_min_size',  [ true, 'Minimum buffer size for pipe reads',  1]),
      OptInt.new('SMB::pipe_read_max_size',  [ true, 'Maximum buffer size for pipe reads', 1024]),
      OptInt.new('SMB::pad_data_level',  [ true, 'Place extra padding between headers and data (level 0-3)', 0]),
      OptInt.new('SMB::pad_file_level',  [ true, 'Obscure path names used in open/create (level 0-3)', 0]),
      OptInt.new('SMB::obscure_trans_pipe_level',  [ true, 'Obscure PIPE string in TransNamedPipe (level 0-3)', 0]),

    ], Msf::Exploit::Remote::SMB)

    register_advanced_options(
    [
      OptBool.new('SMBDirect', [ true, 'The target port is a raw SMB service (not NetBIOS)', true ]),
      OptString.new('SMBUser', [ false, 'The username to authenticate as', '']),
      OptString.new('SMBPass', [ false, 'The password for the specified username', '']),
      OptString.new('SMBDomain',  [ false, 'The Windows domain to use for authentication', '.']),
      OptString.new('SMBName', [ true, 'The NetBIOS hostname (required for port 139 connections)', '*SMBSERVER']),
      OptBool.new('SMB::VerifySignature', [ true, "Enforces client-side verification of server response signatures", false]),
      OptInt.new('SMB::ChunkSize', [ true, 'The chunk size for SMB segments, bigger values will increase speed but break NT 4.0 and SMB signing', 500]),
      #
      # Control the identified operating system of the client
      #
      OptString.new('SMB::Native_OS', [ true, 'The Native OS to send during authentication', 'Windows 2000 2195']),
      OptString.new('SMB::Native_LM', [ true, 'The Native LM to send during authentication', 'Windows 2000 5.0']),

    ], Msf::Exploit::Remote::SMB)

    register_options(
    [
      Opt::RHOST,
      OptInt.new('RPORT', [ true, 'Set the SMB service port', 445])
    ], Msf::Exploit::Remote::SMB)

    register_autofilter_ports([ 139, 445])
    register_autofilter_services(%W{ netbios-ssn microsoft-ds })
  end

  # Override {Exploit::Remote::Tcp#connect} to setup an SMB connection
  # and configure evasion options
  #
  # Also populates {#simple}.
  #
  # @param (see Exploit::Remote::Tcp#connect)
  # @return (see Exploit::Remote::Tcp#connect)
  def connect(global=true)

    disconnect() if global

    s = super(global)
    self.sock = s if global

    # Disable direct SMB when SMBDirect has not been set
    # and the destination port is configured as 139
    direct = smb_direct
    if(datastore.default?('SMBDirect') and rport.to_i == 139)
      direct = false
    end

    c = SIMPLE.new(s, direct)

    # setup pipe evasion foo
    if datastore['SMB::pipe_evasion']
      # XXX - insert code to change the instance of the read/write functions to do segmentation
    end

    if (datastore['SMB::pad_data_level'])
      c.client.evasion_opts['pad_data'] = datastore['SMB::pad_data_level']
    end

    if (datastore['SMB::pad_file_level'])
      c.client.evasion_opts['pad_file'] = datastore['SMB::pad_file_level']
    end

    if (datastore['SMB::obscure_trans_pipe_level'])
      c.client.evasion_opts['obscure_trans_pipe'] = datastore['SMB::obscure_trans_pipe_level']
    end

    self.simple = c if global
    c
  end

  # Convert a standard ASCII string to 16-bit Unicode
  def unicode(str)
    Rex::Text.to_unicode(str)
  end

  # Establishes an SMB session over the default socket and connects to
  # the IPC$ share.
  #
  # You should call {#connect} before calling this
  #
  # @return [void]
  def smb_login
    simple.login(
      datastore['SMBName'],
      datastore['SMBUser'],
      datastore['SMBPass'],
      datastore['SMBDomain'],
      datastore['SMB::VerifySignature'],
      datastore['NTLM::UseNTLMv2'],
      datastore['NTLM::UseNTLM2_session'],
      datastore['NTLM::SendLM'],
      datastore['NTLM::UseLMKey'],
      datastore['NTLM::SendNTLM'],
      datastore['SMB::Native_OS'],
      datastore['SMB::Native_LM'],
      {:use_spn => datastore['NTLM::SendSPN'], :name =>  self.rhost}
    )
    simple.connect("\\\\#{datastore['RHOST']}\\IPC$")
  end


  # This method returns the native operating system of the peer
  def smb_peer_os
    self.simple.client.peer_native_os
  end

  # This method returns the native lanman version of the peer
  def smb_peer_lm
    self.simple.client.peer_native_lm
  end

  # This method opens a handle to an IPC pipe
  def smb_create(pipe)
    self.simple.create_pipe(pipe)
  end

  #the default chunk size of 48000 for OpenFile is not compatible when signing is enabled (and with some nt4 implementations)
  #cause it looks like MS windows refuse to sign big packet and send STATUS_ACCESS_DENIED
  #fd.chunk_size = 500 is better
  def smb_open(path, perm)
    self.simple.open(path, perm, datastore['SMB::ChunkSize'])
  end

  def smb_hostname
    datastore['SMBName'] || '*SMBSERVER'
  end

  def smb_direct
    datastore['SMBDirect']
  end

  def domain
    datastore['SMBDomain']
  end

  def smbhost
    if domain == "."
      "#{rhost}:#{rport}"
    else
      "#{rhost}:#{rport}|#{domain}"
    end
  end

  # If the username contains a / slash, then
  # split it as a domain/username. NOTE: this
  # is predicated on forward slashes, and not
  # Microsoft's backwards slash convention.
  def domain_username_split(user)
    return user if(user.nil? || user.empty?)
    if !user[/\//] # Only /, not \!
      return [nil,user]
    else
      return user.split("/",2)
    end
  end

  def splitname(uname)
    if datastore["PRESERVE_DOMAINS"]
      d,u = domain_username_split(uname)
      return u
    else
      return uname
    end
  end

  # Whether a remote file exists
  #
  # @param file [String] Path to a file to remove, relative to the
  #   most-recently connected share
  # @raise [Rex::Proto::SMB::Exceptions::ErrorCode]
  def smb_file_exist?(file)
    begin
      fd = simple.open(file, 'ro')
    rescue XCEPT::ErrorCode => e
      # If attempting to open the file results in a "*_NOT_FOUND" error,
      # then we can be sure the file is not there.
      #
      # Copy-pasted from smb/exceptions.rb to avoid the gymnastics
      # required to pull them out of a giant inverted hash
      #
      # 0xC0000034 => "STATUS_OBJECT_NAME_NOT_FOUND",
      # 0xC000003A => "STATUS_OBJECT_PATH_NOT_FOUND",
      # 0xC0000225 => "STATUS_NOT_FOUND",
      error_is_not_found = [ 0xC0000034, 0xC000003A, 0xC0000225 ].include?(e.error_code)
      # If the server returns some other error, then there was a
      # permissions problem or some other difficulty that we can't
      # really account for and hope the caller can deal with it.
      raise e unless error_is_not_found
      found = !error_is_not_found
    else
      # There was no exception, so we know the file is openable
      fd.close
      found = true
    end

    found
  end

  # Remove remote file
  #
  # @param file (see #smb_file_exist?)
  # @return [void]
  def smb_file_rm(file)
    fd = smb_open(file, 'ro')
    fd.delete
  end


  #
  # Fingerprinting methods
  #


  # Calls the EnumPrinters() function of the spooler service
  def smb_enumprinters(flags, name, level, blen)
    stub =
      NDR.long(flags) +
      (name ? NDR.uwstring(name) : NDR.long(0)) +
      NDR.long(level) +
      NDR.long(rand(0xffffffff)+1)+
      NDR.long(blen) +
      "\x00" * blen +
      NDR.long(blen)

    handle = dcerpc_handle(
      '12345678-1234-abcd-ef00-0123456789ab', '1.0',
      'ncacn_np', ["\\SPOOLSS"]
    )

    begin
      dcerpc_bind(handle)
      dcerpc.call(0x00, stub)
      return dcerpc.last_response.stub_data
    rescue ::Interrupt
      raise $!
    rescue ::Exception => e
      return nil
    end
  end

  # This method dumps the print provider strings from the spooler
  def smb_enumprintproviders
    resp = smb_enumprinters(8, nil, 1, 0)
    return nil if not resp
    rptr, tmp, blen = resp.unpack("V*")

    resp = smb_enumprinters(8, nil, 1, blen)
    return nil if not resp

    bcnt,pcnt,stat = resp[-12, 12].unpack("VVV")
    return nil if stat != 0
    return nil if pcnt == 0
    return nil if bcnt > blen
    return nil if pcnt < 3

    #
    # The correct way, which leads to invalid offsets :-(
    #
    #providers = []
    #
    #0.upto(pcnt-1) do |i|
    # flags,desc_o,name_o,comm_o = resp[8 + (i*16), 16].unpack("VVVV")
    #
    # #desc = read_unicode(resp,8+desc_o).gsub("\x00", '')
    # #name = read_unicode(resp,8+name_o).gsub("\x00", '')
    # #comm = read_unicode(resp,8+comm_o).gsub("\x00", '')
    # #providers << [flags,desc,name,comm]
    #end
    #
    #providers

    return resp

  end

  # This method performs an extensive set of fingerprinting operations
  def smb_fingerprint
    fprint = {}

    # Connect to the server if needed
    if not self.simple
      connect()
      smb_login()
    end

    fprint['native_os'] = smb_peer_os()
    fprint['native_lm'] = smb_peer_lm()

    # Leverage Recog for SMB native OS fingerprinting
    fp_match = Recog::Nizer.match('smb.native_os', fprint['native_os']) || { }

    os = fp_match['os.product'] || 'Unknown'
    sp = fp_match['os.version'] || ''

    # Metasploit prefers 'Windows 2003' vs 'Windows Server 2003'
    if os =~ /^Windows Server/
      os = os.sub(/^Windows Server/, 'Windows')
    end

    if fp_match['os.edition']
      fprint['edition'] = fp_match['os.edition']
    end

    if fp_match['os.build']
      fprint['build'] = fp_match['os.build']
    end

    if sp == ''
      sp = smb_fingerprint_windows_sp(os)
    end

    lang = smb_fingerprint_windows_lang

    fprint['os']   = os
    fprint['sp']   = sp
    fprint['lang'] = lang

    fprint
  end

  #
  # Determine the service pack level of a Windows system via SMB probes
  #
  def smb_fingerprint_windows_sp(os)
    sp = ''

    if (os == 'Windows XP')
      # SRVSVC was blocked in SP2
      begin
        smb_create("\\SRVSVC")
        sp = 'Service Pack 0 / 1'
      rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e
        if (e.error_code == 0xc0000022)
          sp = 'Service Pack 2+'
        end
      end
    end

    if (os == 'Windows 2000' and sp.length == 0)
      # LLSRPC was blocked in a post-SP4 update
      begin
        smb_create("\\LLSRPC")
        sp = 'Service Pack 0 - 4'
      rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e
        if (e.error_code == 0xc0000022)
          sp = 'Service Pack 4 with MS05-010+'
        end
      end
    end

    #
    # Perform granular XP SP checks if LSARPC is exposed
    #
    if (os == 'Windows XP')

      #
      # Service Pack 2 added a range(0,64000) to opnum 0x22 in SRVSVC
      # Credit to spoonm for first use of unbounded [out] buffers
      #
      handle = dcerpc_handle(
        '4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0',
        'ncacn_np', ["\\BROWSER"]
      )

      begin
        dcerpc_bind(handle)

        stub =
          NDR.uwstring(Rex::Text.rand_text_alpha(rand(10)+1)) +
          NDR.wstring(Rex::Text.rand_text_alpha(rand(10)+1))  +
          NDR.long(64001) +
          NDR.long(0) +
          NDR.long(0)

        dcerpc.call(0x22, stub)
        sp = "Service Pack 0 / 1"

      rescue ::Interrupt
        raise $!
      rescue ::Rex::Proto::SMB::Exceptions::ErrorCode
      rescue ::Rex::Proto::SMB::Exceptions::ReadPacket
      rescue ::Rex::Proto::DCERPC::Exceptions::Fault
        sp = "Service Pack 2+"
      rescue ::Exception
      end


      #
      # Service Pack 3 fixed information leaks via [unique][out] pointers
      # Call SRVSVC::NetRemoteTOD() to return [out] [ref] [unique]
      # Credit:
      #   Pointer leak is well known, but Immunity also covered in a paper
      #   Silent fix of pointer leak in SP3 and detection method by Rhys Kidd
      #
      handle = dcerpc_handle(
        '4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0',
        'ncacn_np', ["\\BROWSER"]
      )

      begin
        dcerpc_bind(handle)

        stub = NDR.uwstring(Rex::Text.rand_text_alpha(rand(8)+1))
        resp = dcerpc.call(0x1c, stub)

        if(resp and resp[0,4] == "\x00\x00\x02\x00")
          sp = "Service Pack 3"
        else
          if(resp and sp =~ /Service Pack 2\+/)
            sp = "Service Pack 2"
          end
        end

      rescue ::Interrupt
        raise $!
      rescue ::Rex::Proto::SMB::Exceptions::ErrorCode
      rescue ::Rex::Proto::SMB::Exceptions::ReadPacket
      rescue ::Exception
      end
    end

    sp
  end


  #
  # Determine the native language pack of a Windows system via SMB probes
  #
  def smb_fingerprint_windows_lang

    #
    # Remote language detection via Print Providers
    # Credit: http://immunityinc.com/downloads/Remote_Language_Detection_in_Immunity_CANVAS.odt
    #

    lang = 'Unknown'

    sigs =
    {
      'English' =>
        [
          Rex::Text.to_unicode('Windows NT Remote Printers'),
          Rex::Text.to_unicode('LanMan Print Services')
        ],
      'Spanish' =>
        [
          Rex::Text.to_unicode('Impresoras remotas Windows NT'),
          Rex::Text.to_unicode('Impresoras remotas de Windows NT')
        ],
      'Italian' =>
        [
          Rex::Text.to_unicode('Stampanti remote di Windows NT'),
          Rex::Text.to_unicode('Servizi di stampa LanMan')
        ],
      'French' =>
        [
          Rex::Text.to_unicode('Imprimantes distantes NT'),
          Rex::Text.to_unicode('Imprimantes distantes pour Windows NT'),
          Rex::Text.to_unicode("Services d'impression LanMan")
        ],
      'German' =>
        [
          Rex::Text.to_unicode('Remotedrucker')
        ],
      'Portuguese - Brazilian' =>
        [
          Rex::Text.to_unicode('Impr. remotas Windows NT'),
          Rex::Text.to_unicode('Impressoras remotas do Windows NT')
        ],
      'Portuguese' =>
        [
          Rex::Text.to_unicode('Imp. remotas do Windows NT')
        ],
      'Hungarian' =>
        [
          Rex::Text.to_unicode("\x54\xe1\x76\x6f\x6c\x69\x20\x6e\x79\x6f\x6d\x74\x61\x74\xf3\x6b")
        ],
      'Finnish' =>
        [
          Rex::Text.to_unicode("\x45\x74\xe4\x74\x75\x6c\x6f\x73\x74\x69\x6d\x65\x74")
        ],
      'Dutch' =>
        [
          Rex::Text.to_unicode('Externe printers voor NT')
        ],
      'Danish' =>
        [
          Rex::Text.to_unicode('Fjernprintere')
        ],
      'Swedish' =>
        [
          Rex::Text.to_unicode("\x46\x6a\xe4\x72\x72\x73\x6b\x72\x69\x76\x61\x72\x65")
        ],
      'Polish' =>
        [
          Rex::Text.to_unicode('Zdalne drukarki')
        ],
      'Czech'   =>
        [
          Rex::Text.to_unicode("\x56\x7a\x64\xe1\x6c\x65\x6e\xe9\x20\x74\x69\x73\x6b\xe1\x72\x6e\x79")
        ],
      'Turkish' =>
        [
          "\x59\x00\x61\x00\x7a\x00\x31\x01\x63\x00\x31\x01\x6c\x00\x61\x00\x72\x00"
        ],
      'Japanese' =>
        [
          "\xea\x30\xe2\x30\xfc\x30\xc8\x30\x20\x00\xd7\x30\xea\x30\xf3\x30\xbf\x30"
        ],
      'Chinese - Traditional' =>
        [
          "\xdc\x8f\x0b\x7a\x53\x62\x70\x53\x3a\x67"
        ],
      'Chinese - Traditional / Taiwan' =>
        [
          "\x60\x90\xef\x7a\x70\x53\x68\x88\x5f\x6a",
        ],
      'Korean' =>
        [
          "\xd0\xc6\xa9\xac\x20\x00\x04\xd5\xb0\xb9\x30\xd1",
        ],
      'Russian' =>
        [
          "\x1f\x04\x40\x04\x38\x04\x3d\x04\x42\x04\x35\x04\x40\x04\x4b\x04\x20\x00\x43\x04\x34\x04\x30\x04\x3b\x04\x35\x04\x3d\x04\x3d\x04\x3e\x04\x33\x04\x3e\x04\x20\x00\x34\x04\x3e\x04\x41\x04\x42\x04\x43\x04\x3f\x04\x30\x04",
        ],

    }

    begin
      prov = smb_enumprintproviders()
      if(prov)
        sigs.each_key do |k|
          sigs[k].each do |s|
            if(prov.index(s))
              lang = k
              break
            end
            break if lang != 'Unknown'
          end
          break if lang != 'Unknown'
        end

        if(lang == 'Unknown')

          @fpcache ||= {}
          mhash = ::Digest::MD5.hexdigest(prov[4,prov.length-4])

          if(not @fpcache[mhash])

            buff = "\n"
            buff << "*** NEW FINGERPRINT: PLEASE SEND TO [ msfdev[at]metasploit.com ]\n"
            buff << " VERS: $Revision$\n"
            buff << " HOST: #{rhost}\n"
            buff << "   OS: #{os}\n"
            buff << "   SP: #{sp}\n"

            prov.unpack("H*")[0].scan(/.{64}|.*/).each do |line|
              next if line.length == 0
              buff << "   FP: #{line}\n"
            end

            prov.split(/\x00\x00+/n).each do |line|
              line.gsub!("\x00",'')
              line.strip!
              next if line.length < 6

              buff <<  "  TXT: #{line}\n"
            end

            buff << "*** END FINGERPRINT\n"

            print_line(buff)

            @fpcache[mhash] = true
          end

        end
      end
    rescue ::Interrupt
      raise $!
    rescue ::Rex::Proto::SMB::Exceptions::ErrorCode
    end
    lang
  end

  # @return [Rex::Proto::SMB::SimpleClient]
  attr_accessor :simple

end


end
